/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2005-2007 Ben Motmans <ben.motmans@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;

using Gdk;
using Gtk;
using Pango;

using Galaxium.Core;
using Galaxium.Protocol;
using Anculus.Core;

namespace Galaxium.Gui.GtkGui
{
	public class TextMessageDisplay : TextView, IMessageDisplay
	{
		internal struct EmoticonMark
		{
			public IEmoticon Emoticon;
			public TextChildAnchor Anchor;
			public Gtk.Image Image;
			public string Equivalent;
		}
		
		protected TextBuffer _textBuffer;
		internal TextTagTable _tagTable;
		protected TextMark _endMark;
		
		List<EmoticonMark> _emoticons = new List<EmoticonMark> ();
		
		protected string _lastIdentifier = String.Empty;
		
		protected TextTag _messageTag;
		protected TextTag _boldTag;
		protected TextTag _italicTag;
		protected TextTag _underlineTag;
		protected TextTag _strikeTag;
		
		protected Cursor _cursorHand;
		protected Cursor _cursorPointer;
		
		protected LinkTag _activeLink;
		protected bool _mouseOverLink;
		protected TextIter _mouseOverIter;
		
		private Gtk.Tooltips _tooltips;
		
		public TextMessageDisplay (IConversation conversation)
		{
			_cursorHand = new Cursor (CursorType.Hand2);
			_cursorPointer = new Cursor (CursorType.Xterm);
			
			Editable = false;
			DoubleBuffered = true;
			WrapMode = Gtk.WrapMode.Word;
			CursorVisible = false;
			LeftMargin = 2;
			RightMargin = 2;
			CanFocus = false;
			
			// Setup a normal message text tag.
			_messageTag = new TextTag("message_tag");
			_messageTag.LeftMargin = 50;
			
			TextTag nonMargineMessageTag = new TextTag("no_margin_message_tag");
			nonMargineMessageTag.LeftMargin = 5;
			
			// Setup a action message text tag.
			TextTag actionTag = new TextTag("action_tag");
			actionTag.Weight = Pango.Weight.Bold;
			actionTag.Scale = 0.8;
			actionTag.PixelsAboveLines = 5;
			actionTag.PixelsBelowLines = 5;
			
			TextTag historyTag = new TextTag("history_tag");
			historyTag.Weight = Pango.Weight.Light;
			historyTag.Foreground = "slate gray";
			historyTag.PixelsAboveLines = 5;
			
			// Setup a warning message text tag.
			TextTag warningTag = new TextTag("warning_tag");
			warningTag.Foreground = "red";
			warningTag.Weight = Pango.Weight.Bold;
			warningTag.Scale = 0.8;
			warningTag.PixelsAboveLines = 5;
			warningTag.PixelsBelowLines = 5;
			
			// Setup a time message text tag.
			TextTag timeTag = new TextTag("time_tag");
			timeTag.Scale = 0.9;
			timeTag.LeftMargin = 10;
			timeTag.Style = Pango.Style.Italic;
		
			// Setup a margin text tag.
			TextTag marginTag = new TextTag("margin_tag");
			marginTag.LeftMargin = 10;
			
			// Setup a bold message text tag.
			_boldTag = new TextTag("bold_tag");
			_boldTag.Weight = Pango.Weight.Ultrabold;
			
			// Setup a italic message text tag.
			_italicTag = new TextTag("italic_tag");
			_italicTag.Style = Pango.Style.Italic;
			
			// Setup a underline message text tag.
			_underlineTag = new TextTag("underline_tag");
			_underlineTag.Underline = Pango.Underline.Single;
			
			// Setup a strikethrough message text tag.
			_strikeTag = new TextTag("strike_tag");
			_strikeTag.Strikethrough = true; 
			
			// Add the above text tags to the table.
			_tagTable = new TextTagTable();
			_tagTable.Add(timeTag);
			_tagTable.Add(marginTag);
			_tagTable.Add(historyTag);
			_tagTable.Add(_messageTag);
			_tagTable.Add(nonMargineMessageTag);
			_tagTable.Add(actionTag);
			_tagTable.Add(warningTag);
			_tagTable.Add(_boldTag);
			_tagTable.Add(_italicTag);
			_tagTable.Add(_underlineTag);
			_tagTable.Add(_strikeTag);
			
			_textBuffer = new TextBuffer (_tagTable);
			this.Buffer = _textBuffer;
			
			_tooltips = new Gtk.Tooltips();
			
			this.ButtonReleaseEvent += new ButtonReleaseEventHandler (OnTextViewButtonRelease);
			ShowAll ();
		}
		
		public TextBuffer TextBuffer
		{
			get { return _textBuffer; }
		}
		
		public object Widget
		{
			get { return this; }
		}
		
		public virtual void Clear ()
		{
			_textBuffer.Clear ();
		}
		
		public virtual void AddMessage (ITextMessage message)
		{
			if(!_lastIdentifier.Equals(message.Source.UniqueIdentifier))
			{
				WriteFrom(message.Source);
				WriteNewLine();
			}
			
			_lastIdentifier = message.Source.UniqueIdentifier;
			
			WriteMargin();
			WriteTime(message.TimeStamp);
			
			WriteMessage(message);
			
			WriteNewLine();
			AutoScroll ();
		}
		
		public virtual void AddHistoryMessage (ITextMessage message)
		{
			foreach (ITextChunk chunk in message.Chunks)
				chunk.Style.ForeColor = 8947848;
			
			AddMessage (message);
		}
		
		public virtual void AddOfflineMessage (ITextMessage message)
		{
			if(!_lastIdentifier.Equals(message.Source.UniqueIdentifier + " [Offline]"))
			{
				WriteFromOffline(message.Source);
				WriteNewLine();
			}
			
			_lastIdentifier = message.Source.UniqueIdentifier + " [Offline]";
			
			WriteMargin();
			WriteTime(message.TimeStamp);
			
			WriteMessage(message);
			
			WriteNewLine();
			AutoScroll ();
		}
		
		public virtual void AddImageMessage (IEntity source, byte[] data)
		{
			if(!_lastIdentifier.Equals (source.UniqueIdentifier))
			{
				WriteFrom (source);
				WriteNewLine ();
			}
			
			_lastIdentifier = source.UniqueIdentifier;
			
			WriteMargin ();
			WriteTime (DateTime.Now);
			
			WriteImage (data);
			
			WriteNewLine ();
			AutoScroll ();
		}
		
		public virtual void AddSystemMessage(string message)
		{
			string format = String.Empty;
			
			//if (string.IsNullOrEmpty (format))
				format = DateTime.Now.ToShortTimeString ();
			//else
				//format = DateTime.Now.ToString (format);
			
			TextIter endIter = _textBuffer.EndIter;
			_textBuffer.InsertWithTagsByName(ref endIter, "["+format+"] "+message, "action_tag");
			
			WriteNewLine();
			AutoScroll ();
			
			_textBuffer.SelectRange(_textBuffer.EndIter, _textBuffer.EndIter);
			
			_lastIdentifier = String.Empty;
		}
		
		public virtual void UpdateEmoticon(IEmoticon emot)
		{
			//emoticon has been received, we need to show it wherever it has been used
			
			for (int i = 0; i < _emoticons.Count; i++)
			{
				EmoticonMark mark = _emoticons[i];
				
				if (mark.Emoticon != emot)
					continue;
				
				Gtk.Image newImage = LoadEmoticon (emot);
				
				if (newImage == null)
					continue;
				
				_tooltips.SetTip (newImage, string.Format("{0} ({1})", emot.Name, mark.Equivalent), null);
				
				Remove (mark.Image);
				mark.Image = newImage;
				AddChildAtAnchor (mark.Image, mark.Anchor);
			}
		}
		
		internal static Gtk.Image LoadImage (byte[] data)
		{
			try
			{
				PixbufAnimation anim = null;
				Pixbuf pixbuf = null;
				
				if ((data != null) && (data.Length > 0))
				{
					PixbufLoader loader = new PixbufLoader (data);
						
					if (loader.Animation.IsStaticImage)
						pixbuf = loader.Pixbuf;
					else
						anim = loader.Animation;
				}	
				else	
					pixbuf = IconUtility.GetIcon ("galaxium-image");
				
				Gtk.Image image;
				
				if (anim != null)
					image = new Gtk.Image (anim);
				else
					image = new Gtk.Image (pixbuf);
				
				image.Visible = true;
				
				return image;
			}
			catch (Exception)
			{
				return new Gtk.Image (IconUtility.GetIcon ("galaxium-image"));
			}
		}
		
		internal static Gtk.Image LoadEmoticon (IEmoticon emot)
		{
			return LoadImage (emot.Data);
		}
		
		protected virtual void WriteFromOffline (IEntity from)
		{
			TextIter endIter = _textBuffer.EndIter;
			_textBuffer.InsertWithTags(ref endIter,
				string.IsNullOrEmpty(from.DisplayName) ? from.UniqueIdentifier : from.DisplayName + " [Offline]",
				new EntityTag(this, from));
		}
		
		protected virtual void WriteFrom (IEntity from)
		{
			TextIter endIter = _textBuffer.EndIter;
			_textBuffer.InsertWithTags(ref endIter,
				string.IsNullOrEmpty(from.DisplayName) ? from.UniqueIdentifier : from.DisplayName,
				new EntityTag(this, from));
		}
		
		protected virtual void WriteNewLine ()
		{
			TextIter endIter = _textBuffer.EndIter;
			_textBuffer.Insert(ref endIter, Environment.NewLine);
		}
		
		protected virtual void WriteUnformattedText (string text)
		{
			TextIter endIter = _textBuffer.EndIter;
			_textBuffer.Insert(ref endIter, text);
		}
		
		protected virtual void WriteTime (DateTime timestamp)
		{
			string format = String.Empty;
			
			//if (string.IsNullOrEmpty (format))
				format = timestamp.ToShortTimeString ();
			//else
				//format = timestamp.ToString (format);
			
			TextIter endIter = _textBuffer.EndIter;
			_textBuffer.InsertWithTagsByName(ref endIter, String.Concat ("(", format, ") "), "time_tag");
		}
		
		protected virtual void WriteMargin ()
		{
			TextIter endIter = _textBuffer.EndIter;
			_textBuffer.InsertWithTagsByName(ref endIter, "", "margin_tag");
		}
		
		protected virtual void WriteImage (byte[] data)
		{
			TextIter endIter = _textBuffer.EndIter;
			Gtk.TextChildAnchor anchor = _textBuffer.CreateChildAnchor (ref endIter);
			
			AddChildAtAnchor (LoadImage (data), anchor);
		}
		
		protected virtual void WriteMessage (ITextMessage message)
		{
			TextMark firstMark = _textBuffer.CreateMark(null, _textBuffer.EndIter, true);
			
			foreach (ITextChunk chunk in message.Chunks)
			{
				ITextStyle style = chunk.Style;
				
				TextMark beginMark = _textBuffer.CreateMark (null, _textBuffer.EndIter, true);
				TextIter beginIter = _textBuffer.EndIter;
				
				if (chunk.Type == TextChunkType.Emoticon)
				{
					IEmoticon emot = ((EmoticonTextChunk)chunk).Emoticon;
					
					EmoticonMark mark = new EmoticonMark();
					mark.Equivalent = chunk.Text;
					mark.Emoticon = emot;
					mark.Anchor = _textBuffer.CreateChildAnchor (ref beginIter);
					mark.Image = LoadEmoticon (emot);
					
					if (mark.Image == null)
						continue;
					
					_tooltips.SetTip (mark.Image, string.Format("{0} ({1})", emot.Name, chunk.Text), null);
					
					_emoticons.Add (mark);
					
					AddChildAtAnchor (mark.Image, mark.Anchor);
				}
				else
				{
					WriteUnformattedText (chunk.Text);
					
					beginIter = _textBuffer.GetIterAtMark(beginMark);
					TextIter endIter = _textBuffer.EndIter;
					
					if (style == null || style.IsDefault)
						continue;
				
					if (style.IsHyperlink || (chunk.Type == TextChunkType.URL))
					{
						_textBuffer.ApplyTag (new LinkTag (_tagTable, chunk.Text), beginIter, endIter);
					}
					else
					{
						if (style.Bold)
							_textBuffer.ApplyTag (_boldTag, beginIter, endIter);
						if (style.Underline)
							_textBuffer.ApplyTag (_underlineTag, beginIter, endIter);
						if (style.Italic)
							_textBuffer.ApplyTag (_italicTag, beginIter, endIter);
						if (style.Strikethrough)
							_textBuffer.ApplyTag (_strikeTag, beginIter, endIter);
						
						string tagName = null;
						if (style.ForeColor >= 0) {
							tagName = "color_" + style.ForeColor.ToString ();
							Gtk.TextTag colorTag = _tagTable.Lookup (tagName);
							if (colorTag == null) {
								colorTag = new Gtk.TextTag (tagName);
							
								byte r = (byte)((style.ForeColor & 0xFF0000) >> 16);
								byte g = (byte)((style.ForeColor & 0xFF00) >> 8);
								byte b = (byte)(style.ForeColor & 0xFF);
								
								colorTag.ForegroundGdk = new Gdk.Color (r, g, b);
								_tagTable.Add (colorTag);
							}
							_textBuffer.ApplyTag (colorTag, beginIter, endIter);
						}//TODO: bg color support
						
						if (!style.IsDefaultFont) {
							if (style.Font != null) {
								tagName = "font_" + style.Font;
								Gtk.TextTag fontTag = _tagTable.Lookup (tagName);
								if (fontTag == null) {
									fontTag = new Gtk.TextTag (tagName);
									
									fontTag.Font = style.Font;
									_tagTable.Add (fontTag);
								}
								_textBuffer.ApplyTag (fontTag, beginIter, endIter);
							}
							
							if (style.FontSize > 0) {
								tagName = "fontsize_" + style.FontSize;
								Gtk.TextTag fontSizeTag = _tagTable.Lookup (tagName);
								if (fontSizeTag == null) {
									fontSizeTag = new Gtk.TextTag (tagName);
									
									fontSizeTag.FontDesc.Size = style.FontSize;
									_tagTable.Add (fontSizeTag);
								}
								_textBuffer.ApplyTag (fontSizeTag, beginIter, endIter);
								
							}
						}
					}
				}
			}
			
			TextIter firstIter = _textBuffer.GetIterAtMark(firstMark);
			TextIter lastIter = _textBuffer.EndIter;
			_textBuffer.ApplyTag (_messageTag, firstIter, lastIter);
			_textBuffer.DeleteMark(firstMark);
			
			_textBuffer.SelectRange(_textBuffer.EndIter, _textBuffer.EndIter);
		}
		
		protected virtual void WriteWithAttributes(string text, string[] attributes, bool emoticons)
		{
			// Keep the iteration of where we started.
			TextIter endIter = _textBuffer.EndIter;
			
			if (!emoticons)
			{
				_textBuffer.InsertWithTagsByName(ref endIter, text, attributes);
				return;
			}
			
			string temp = text;
			string emoticon = String.Empty;
			int index = 0;
			bool found = false;
			
			/*while(sTemp.Length > 0)
			{
				int iTempIndex = sTemp.Length;
				
				// Go through each emoticon.
				foreach (string sTempEmoticon in EmoticonBasket.EmoticonTable.Keys)
				{
					// If the emoticon exists, keep track of where.
					int iIndexOfEmoticon = sTemp.IndexOf(sTempEmoticon);
					
					if (iIndexOfEmoticon >= 0)
					{
						// Make sure we only keep the lowest index.
						if (iIndexOfEmoticon < iTempIndex)
						{
							iTempIndex = iIndexOfEmoticon;
							sEmoticon = sTempEmoticon;
							bFound = true;
						}
					}
				}
				
				if (bFound)
				{
					string sBefore = sTemp.Substring(iCurrentIndex, iTempIndex);
					
					_textBuffer.InsertWithTagsByName(ref endIter, sBefore, aAttributes);
					
					// Insert the emoticon itself.
					//_textBuffer.InsertPixbuf(ref endIter, EmoticonBasket.Get_Emoticon(sEmoticon).Pixbuf);
					
					sTemp = sTemp.Substring(sBefore.Length+sEmoticon.Length, sTemp.Length - (sBefore.Length+sEmoticon.Length));
					
					bFound = false;
				}
				else
				{
					break;
				}
			}*/
			
			_textBuffer.InsertWithTagsByName(ref endIter, temp, attributes);
			
			_textBuffer.SelectRange(_textBuffer.EndIter, _textBuffer.EndIter);
		}
		
		void AutoScroll ()
		{
			int endY, endHeight;
			base.GetLineYrange (_textBuffer.EndIter, out endY, out endHeight);
			
			if (VisibleRect.Y >= (endY + endHeight - (VisibleRect.Height * 1.2)))
			{
				TextMark endMark = _textBuffer.CreateMark ("end", _textBuffer.EndIter, false);
				ScrollToMark (endMark, 0.4, true, 0.0, 1.0);
				_textBuffer.DeleteMark (endMark);
			}
		}
		
		protected void OnTextViewButtonRelease (object o, ButtonReleaseEventArgs args)
		{
			Gtk.Menu menu = null;
			
			if (_mouseOverIter.ChildAnchor != null)
			{
				IEmoticon emot = null;
				
				foreach (EmoticonMark mark in _emoticons)
				{
					if (mark.Anchor != _mouseOverIter.ChildAnchor)
						continue;
					
					emot = mark.Emoticon;
					break;
				}
				
				if (emot != null)
					menu = MenuUtility.CreateContextMenu ("/Galaxium/Gui/MessageDisplay/Emoticons/Menu", new DefaultExtensionContext (emot));
			}
			else
			{
				//TODO: uncomment + pass correct extension path
				//Menu menu = MenuUtility.CreateContextMenu ("/Galaxium/", new DefaultExtensionContext (this));
			}
			
			if (menu != null)
			{
				menu.Popup (null, null, null, args.Event.Button, args.Event.Time);
				menu.ShowAll ();
			}
		}
		
		protected override bool OnKeyPressEvent (Gdk.EventKey e)
		{
			base.OnKeyPressEvent (e);
			return false;
		}
		
		protected override bool OnButtonPressEvent (Gdk.EventButton e)
		{
			if ((e.Button == 3) && (_mouseOverIter.ChildAnchor != null)) //disable right click menu
				return false;
			
			if ((e.Button == 1) && (_activeLink != null))
				BaseUtility.SafeOpenURL (_activeLink.URL);
			
			return base.OnButtonPressEvent (e);
		}
		
		protected override bool OnMotionNotifyEvent (EventMotion evnt)
		{
			bool ret = base.OnMotionNotifyEvent (evnt);
			
			int x, y;
			WindowToBufferCoords (TextWindowType.Widget, (int)evnt.X, (int)evnt.Y, out x, out y);
			ChangeCursorIfNeeded (x, y);
			return ret;
		}
		
		protected override bool OnVisibilityNotifyEvent (EventVisibility evnt)
		{
			bool ret = base.OnVisibilityNotifyEvent (evnt);
			
			int wx, wy, x, y;
			GetPointer (out wx, out wy);
			WindowToBufferCoords (TextWindowType.Widget, wx, wy, out x, out y);
			ChangeCursorIfNeeded (x, y);	
			
			return ret;
		}
		
		protected virtual void ChangeCursorIfNeeded (int x, int y)
		{
			bool mouseOver = false;
			LinkTag linkTag = null;
			
			_mouseOverIter = GetIterAtLocation (x, y);
			foreach (TextTag tag in _mouseOverIter.Tags)
			{
				if (tag is LinkTag)
				{
					linkTag = (LinkTag)tag;
					mouseOver = true;
					
					break;
				}
			}
			
			if (mouseOver != _mouseOverLink)
			{
				_mouseOverLink = mouseOver;
				
				GetWindow (TextWindowType.Text).Cursor = mouseOver ? _cursorHand : _cursorPointer;
				_activeLink = mouseOver ? linkTag : null;
			}
		}
	}
}